﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Windows.Navigation;
using Common.Exceptions.UserExceptions;
using System.Windows.Data;

namespace Simhopp.Common
{
    /// <summary>
    /// The Competition class provides a description of a diving competition.
    /// </summary>
[Serializable]
    public class Competition : IDatabaseObject, ISerializable
    {
        #region FIELD REGION

        private int id;
        private ObservableCollection<Diver> participants;
        private ReadOnlyObservableCollection<Diver> readOnly_participants;
        private static Collection<DivingType> divingTypeList;
        private static ReadOnlyCollection<DivingType> readOnly_divingTypeList;
        private ObservableCollection<User> judges;
        private ReadOnlyObservableCollection<User> readOnly_judges;
        private ObservableCollection<Heights> heights;
        private ListCollectionView onlyJudges;
        private Genders gender;
        private int currentDiver;
        private Rounds currentRound;
        private int numberOfJudges;
        private string notInProgress;

        #endregion
        #region CONSTRUCTOR REGION
        /// <summary>
        /// Default constructor.
        /// </summary>
        public Competition()
        {
            id = -1;
            divingTypeList = new Collection<DivingType>();
            participants = new ObservableCollection<Diver>();
            judges = new ObservableCollection<User>();
            heights = new ObservableCollection<Heights>(new Heights[3]);
            numberOfJudges = 7;
            currentRound = Rounds.Configure;
            divingTypeList = new Collection<DivingType>();
            readOnly_divingTypeList = new ReadOnlyCollection<DivingType>(divingTypeList);
            readOnly_participants = new ReadOnlyObservableCollection<Diver>(participants);
            readOnly_judges = new ReadOnlyObservableCollection<User>(judges);
            LoadDivingTypesFromFile();
            currentDiver = 0;
            Gender = Genders.Male;
            Name = "";
            City = "";
            Country = "";
            Date = "";
            NotInProgress = "true";
        }
        #endregion
        #region PROPERTY REGION
        /// <summary>
        /// Gets or sets the value of the id-field used in the database.
        /// </summary>
        public int Id
        {
            get { return id; }
            set
            {
                id = value;
                foreach (var diver in participants)
                    diver.CompetitionId = id;
                foreach (var judge in judges)
                    judge.CompetitionId = id;
            }
        }
        
        /// <summary>
        /// Gets or sets the string property indicating whether the competition is in progress.
        /// </summary>
        public String NotInProgress    
        {
            get { return notInProgress; }
            set
            {
                if (value.CompareTo("false") == 0 || value.CompareTo("true") == 0)
                {
                    notInProgress = value;
                }
            }
        }

        /// <summary>
        /// Gets the participants list as read only.
        /// </summary>
        public ReadOnlyObservableCollection<Diver> Participants
        {
            get { return readOnly_participants; }
        }

        /// <summary>
        /// Gets the judges list as read only.
        /// </summary>
        public ReadOnlyObservableCollection<User> Judges
        {
            get { return readOnly_judges; }
        }

        /// <summary>
        /// Gets the judges list as read only.
        /// </summary>
        public ListCollectionView JudgesOnly
        {
            get
            {
                if (this.onlyJudges == null)
                {
                    this.onlyJudges = new ListCollectionView(judges);
                    this.onlyJudges.Filter += delegate(object obj)
                    {
                        if (obj as User != null)
                        {
                            if ((obj as User).AccountType == AccountTypes.Judge || (obj as User).AccountType == AccountTypes.HeadJudge)
                            {
                                return true;
                            }
                        }
                        return false;
                    };
                }
                return this.onlyJudges;
            }
            set
            {
                onlyJudges = value;
            }
        }

        /// <summary>
        /// Gets the dive type list as read only.
        /// </summary>
        public static ReadOnlyCollection<DivingType> DivingTypeList
        {
            get { return readOnly_divingTypeList; }
        }

        /// <summary>
        /// Gets the static height list as read only.
        /// </summary>
        public Collection<Heights> Heights
        {
            get { return heights; }
        }

        /// <summary>
        /// Gets the static round number of the current round.
        /// </summary>
        public Rounds CurrentRound
        {
            get { return currentRound; }
            set { currentRound = value; }
        }
        
        /// <summary>
        /// Gets or sets the index at which the current diver resides in the participants list.
        /// </summary>
        public int CurrentDiverIndex
        {
            get { return currentDiver; }
            set { currentDiver = value; }
        }

        /// <summary>
        /// Gets and sets the number of judges in the competition.
        /// </summary>
        public int NumberOfJudges
        {
            get { return numberOfJudges; }
            set
            {
                if (value != 5 && value != 7)
                {
                    throw new ArgumentException("The number of judges must be either 5 or 7.");
                }
                numberOfJudges = value;
            }
        }

        /// <summary>
        /// Gets and sets the number of judges in the competition (enum JudgeCount).
        /// </summary>
        public JudgeCount NJudgeCount
        {
            get { return (JudgeCount) numberOfJudges; }
            set { numberOfJudges = (int) value; }
        }

        /// <summary>
        /// Gets and sets the gender of the participants allowed in the competition.
        /// </summary>
        public Genders Gender
        {
            get { return gender; }

            set
            {
                if (gender != value)
                {
                    RemoveAllParticipants();
                }
                gender = value;
            }
        }

        /// <summary>
        /// Gets and sets the name of the competition.
        /// </summary>
        public String Name { get; set; }

        /// <summary>
        /// Gets and sets the city in which the competition is taking place.
        /// </summary>
        public String City { get; set; }

        /// <summary>
        /// Gets and sets the country in which the competition is taking place.
        /// </summary>
        public String Country { get; set; }

        /// <summary>
        /// Gets and sets the date when the competition is taking place.
        /// </summary>
        public String Date { get; set; }

        #endregion
        #region HELPER METHOD REGION

        /// <summary>
        /// Private helper method used for randomizing the participant list for the first round.
        /// </summary>
        /// <param name="diverList"></param>
        private static void Randomize(IList<Diver> diverList)
        {
            var r = new Random();
            for (var i = diverList.Count; i > 0; i--)
            {
                int j = r.Next(i);
                Diver diver = diverList[j];
                diverList[j] = diverList[i - 1];
                diverList[i - 1] = diver;
            }
        }
        
        /// <summary>
        /// Private helper method used for sorting the participant list after the first round.
        /// </summary>
        /// <param name="diverList"></param>
        private static void Sort(ObservableCollection<Diver> diverList)
        {
            var localList = diverList.ToList();
            localList.Sort();
            diverList.Clear();
            foreach (var diver in localList)
            {
                diverList.Add(diver);
            }
        }
        
        /// <summary>
        /// Private helper method used to update the dive heights before the start of the competition.
        /// </summary>
        private void UpdateDives()
        {
            foreach (var participant in participants)
                participant.UpdateDives(Heights);
        }
        
        /// <summary>
        /// Private helper method used to update the judge property HasSubmittedScore to false for all the judges.
        /// </summary>
        private void ResetJudges()
        {
            foreach (var judge in judges)
                judge.HasSubmittedScore = false;
        }
        
        /// <summary>
        /// Private helper method used to check if the specified diver is participating in the competition.
        /// </summary>
        /// <param name="diver">The diver to check</param>
        /// <returns>True/false</returns>
        private bool IsParticipating(Diver diver)
        {
            var found = false;
            foreach (var participant in participants)
            {
                if (participant.Equals(diver))
                    found = true;
            }
            return found;
        }

        #endregion
        #region METHOD REGION

        /// <summary>
        /// Checks if two instances of the object are identical
        /// </summary>
        /// <param name="obj">The object to compare with</param>
        /// <returns>True/False</returns>
        public override bool Equals(object obj)
        {
            return ((obj as Competition) != null) && (obj as Competition).Id == Id;
        }

        /// <summary>
        /// Used in addition to Equals method
        /// </summary>
        /// <returns>A unique id</returns>
        public override int GetHashCode()
        {
            return id;
        }
        
        /// <summary>
        /// Returns the index of the current round (0, 1, 2)
        /// </summary>
        /// <returns>An integer</returns>
        public int GetIndexOfCurrentRound()
        {
            return (int) currentRound;
        }

        /// <summary>
        /// Adds a specified participant to the competition.
        /// </summary>
        /// <param name="diver">The diver to add.</param>
        public void AddParticipant(Diver diver)
        {
            if (diver.Gender != gender)
                throw new ArgumentException("Wrong gender");
            participants.Add(diver);
        }

        /// <summary>
        /// Removes a specified participant from the competition.
        /// </summary>
        /// <param name="diver">The diver to remove.</param>
        /// <returns></returns>
        public bool RemoveParticipant(Diver diver)
        {
            return participants.Remove(diver);
        }

        /// <summary>
        /// Removes all participants from the competition.
        /// </summary>
        public void RemoveAllParticipants()
        {
            participants.Clear();
        }

        /// <summary>
        /// Adds a specified judge to the competition.
        /// </summary>
        /// <param name="judge">The judge to add.</param>
        public void AddJudge(User judge)
        {
            if (judge.AccountType == AccountTypes.Administrator)
            {
                judges.Add(judge);
                return;
            }
            if (NoOfJudges() == numberOfJudges){
                throw new JudgeListFullException(String.Format("There are already {0} judges in the competition!", NumberOfJudges));
                
            }
            if (judge != null && judge.AccountType != AccountTypes.Judge && judge.AccountType != AccountTypes.HeadJudge)
                throw new WrongUserException("Only Judges and HeadJudges can be added to a competition");
            judges.Add(judge);
        }
        
        /// <summary>
        /// Gets the number of judges currently in the competition.
        /// </summary>
        /// <returns></returns>
        public int NoOfJudges()
        {
            return judges.Count(user => user.AccountType == AccountTypes.Judge || user.AccountType == AccountTypes.HeadJudge);
        }

        /// <summary>
        /// Remove a specified judge from the competition.
        /// </summary>
        /// <param name="judge">The judge to remove.</param>
        /// <returns></returns>
        public bool RemoveJudge(User judge)
        {
            return judges.Remove(judge);
        }

        /// <summary>
        /// Removes all judges from the competition.
        /// </summary>
        public void RemoveAllJudges()
        {
            judges.Clear();
        }

        /// <summary>
        /// Proceeds to the next round if all the conditions are met.
        /// </summary>
        public void NextRound()
        {
            if (currentRound == Rounds.Configure && IsConfigured())
            {
                UpdateDives();
                Randomize(participants);
            }
            else if (currentRound != Rounds.Configure &&
                     CurrentRoundIsScored() &&
                     currentRound < Rounds.ThirdRound)
            {
                Sort(participants);
            }
            else
            {
                throw new ArgumentException("Cannot move to the next round.");
            }
            ResetJudges();
            currentRound++;
            currentDiver = 0;
        }

        /// <summary>
        /// Proceeds to the next diver if all the conditions are met.
        /// </summary>
        public void NextDiver()
        {
            if (!GetCurrentDiver().CurrentRoundIsScored(currentRound,NumberOfJudges))
            {
                throw new ArgumentException("Cannot move to the next diver.");
            }
            ResetJudges();
            currentDiver++;
        }

        /// <summary>
        /// Gets the current diver.
        /// </summary>
        /// <returns>The diver that is currently performing a dive.</returns>
        public Diver GetCurrentDiver()
        {
            if (currentRound == Rounds.Configure)
            {
                throw new ArgumentException("The competition is being configured.");
            }
            return participants[currentDiver];
        }

        /// <summary>
        /// Gets the next diver in the list.
        /// </summary>
        /// <returns>The next diver to perform a dive.</returns>
        public Diver GetNextDiver()
        {
            if (currentDiver + 1 == participants.Count)
            {
                //Changed this to ArgumentException instead.
                //FxCop says managed code shouldn't throw IndexOutOfRangeException.
                //throw new IndexOutOfRangeException("No next diver in list");
                throw new ArgumentException("No next driver in list.");
            }

            return participants[currentDiver + 1];
        }

        /// <summary>
        /// Loads a set of dive types to the property DivingTypeList.
        /// </summary>
        /// 
        public void LoadDivingTypesFromFile()
        {
            var sr = new StreamReader("Classes/hopp2.txt");

            var keyId = 0;

            while (!sr.EndOfStream)
            {
                var temp = new DivingType();

                var line = sr.ReadLine();

                if (line != null)
                {
                    string[] sp = line.Split('\t');

                    sp[1] = sp[1].Remove(sp[1].Length - 1);

                    int tempIntHeight = Convert.ToInt16(sp[1]);
                    double tempDoublePointsMultiplier = Convert.ToDouble(sp[3], CultureInfo.InvariantCulture);



                    temp.ID = sp[0];
                    // Temp.KeyId = keyId;
                    temp.TypeName = sp[2];
                    temp.Height = (Heights)tempIntHeight;
                    temp.DifficultyFactor = tempDoublePointsMultiplier;
                    temp.KeyId = keyId;
                }
                divingTypeList.Add(temp);

                keyId++;
            }

        }

        /// <summary>
        /// Checks to see whether the competition is configured and ready to start.
        /// </summary>
        /// <returns>True if the competition is properly configured, otherwise False.</returns>
        public bool IsConfigured()
        {

            return participants.All(participant => participant.IsConfigured()) &&
                   JudgesOnly.Count == (int)NJudgeCount &&
                   Heights.Count == 3;

        }

        /// <summary>
        /// Checks to see whether the divers have received 
        /// scores from all the judges in the current round.
        /// </summary>
        /// <returns>True if the current round is scored, otherwise False.</returns>
        public bool CurrentRoundIsScored()
        {
            return participants.All(participant => participant.CurrentRoundIsScored(currentRound, NumberOfJudges));
        }
        
        #endregion
        #region DATABASE REGION

        /// <summary>
        /// Creates a dictionary from the object where member field names are mapped to their corresponding values
        /// </summary>
        /// <returns>A dictionary</returns>
        public Dictionary<String, Object> GetDictionary()
        {
            try
            {
                var dictionary = new Dictionary<String, Object>
                {
                    {"Id", Id.ToString(CultureInfo.InvariantCulture)},
                    {"Name", Name.ToString(CultureInfo.InvariantCulture)},
                    {"City", City.ToString(CultureInfo.InvariantCulture)},
                    {"Country", Country.ToString(CultureInfo.InvariantCulture)},
                    {"Gender", (int)Gender},
                    {"Date", Date.ToString(CultureInfo.InvariantCulture)},
                    {"NumberOfJudges", NumberOfJudges},
                    {"CurrentDiver", currentDiver.ToString(CultureInfo.InvariantCulture)},
                    {"CurrentRound", ((int)currentRound).ToString(CultureInfo.InvariantCulture)},
                    {"NotInProgress", NotInProgress.ToString(CultureInfo.InvariantCulture)}
                };
                if (Heights.Count > 0)
                    dictionary.Add("Height1", (int)Heights[0]);
                if (Heights.Count > 1)
                    dictionary.Add("Height2", (int)Heights[1]);
                if (Heights.Count > 2)
                    dictionary.Add("Height3", (int)Heights[2]);
                return dictionary;
            }
            catch (Exception e)
            {
                MyLogger.Error("Failed to create dictionary from Competition object - " + e.Message);
                return null;
            }
        }

        /// <summary>
        /// Creates an Competition object from a passed dictionary
        /// </summary>
        /// <param name="data">A dictionary where database field names are mapped to their corresponding values</param>
        /// <returns>A Competition object</returns>
        public static Competition GetFromDictionary(Dictionary<String, Object> data)
        {
            try
            {
                var competition = new Competition
                {
                    Id = (int)data["Id"],
                    Name = (String)data["Name"],
                    City = (String)data["City"],
                    Country = (String)data["Country"],
                    Gender = (Genders)data["Gender"],
                    Date = (String)data["Date"],
                    currentDiver = (int)data["CurrentDiver"],
                    currentRound = (Rounds)data["CurrentRound"],
                    notInProgress = (String)data["NotInProgress"]
                };
                competition.Heights[0] = ((Heights) data["Height1"]);
                competition.Heights[1] = ((Heights) data["Height2"]);
                competition.Heights[2] = ((Heights) data["Height3"]);
                competition.NumberOfJudges = (int) data["NumberOfJudges"];
                return competition;
            }
            catch (Exception e)
            {
                MyLogger.Error("Failed to create Competition object from dictionary - " + e.Message);
                return null;
            }
        }

        /// <summary>
        /// Returns the name of the Id field used in the corresponding database-table
        /// </summary>
        /// <returns></returns>
        public String GetIdFieldName()
        {
            return "Id";
        }

        /// <summary>
        /// Gets the table name as a string.
        /// </summary>
        /// <returns>The table name as a string.</returns>
        public String GetTableName()
        {
            return "Competitions";
        }
        #endregion
        #region SERIALIZABLE/DESERIALIZABLE
        /// <summary>
        /// Adds the class specific information used in the serialization of an object.
        /// </summary>
        /// <param name="info">The class specific information.</param>
        /// <param name="context">Describes the source and destination of a given serialized stream.</param>
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("name", Name, typeof(string));
            info.AddValue("date", Date, typeof(string));
            info.AddValue("country", Country, typeof(string));
            info.AddValue("city", City, typeof(string));
            info.AddValue("id", id, typeof(int));
            info.AddValue("participants", participants, typeof(ObservableCollection<Diver>));
            info.AddValue("judges", judges, typeof(ObservableCollection<User>));
            info.AddValue("heights", heights, typeof(ObservableCollection<Heights>));
            info.AddValue("gender", gender, typeof(Genders));
            info.AddValue("currentDiver", currentDiver, typeof(int));
            info.AddValue("currentRound", currentRound, typeof(Rounds));
            info.AddValue("numberOfJudges", numberOfJudges, typeof(int));
            info.AddValue("notInProgress", notInProgress, typeof(String));
        }
        
        /// <summary>
        /// Constructor used in the deserialization of an object.
        /// </summary>
        /// <param name="info">The class specific information.</param>
        /// <param name="context">Describes the source and destination of a given serialized stream.</param>
        protected Competition(SerializationInfo info, StreamingContext context) 
        {
            divingTypeList = new Collection<DivingType>();
            LoadDivingTypesFromFile();

            Name = (string)info.GetValue("name", typeof(string));
            City = (string)info.GetValue("country", typeof(string));
            Country = (string)info.GetValue("city", typeof(string));
            Date = (string)info.GetValue("date", typeof(string));
            id = (int)info.GetValue("id", typeof(int));
            participants = (ObservableCollection<Diver>) info.GetValue("participants", typeof(ObservableCollection<Diver>));
            judges = (ObservableCollection<User>) info.GetValue("judges", typeof(ObservableCollection<User>));
            heights = (ObservableCollection<Heights>) info.GetValue("heights", typeof(ObservableCollection<Heights>));
            gender = (Genders) info.GetValue("gender", typeof(Genders));
            currentDiver = (int) info.GetValue("currentDiver", typeof(int));
            currentRound = (Rounds) info.GetValue("currentRound",  typeof(Rounds));
            numberOfJudges = (int)info.GetValue("numberOfJudges", typeof(int));
            notInProgress = (String)info.GetValue("notInProgress", typeof(String));

            readOnly_divingTypeList = new ReadOnlyCollection<DivingType>(divingTypeList);
            readOnly_participants = new ReadOnlyObservableCollection<Diver>(participants);
            readOnly_judges = new ReadOnlyObservableCollection<User>(judges);

        }
        #endregion
    }
}
